#include "meshBase.hpp"
#include "meshTypes.hpp"
#include "exceptions.hpp"
#include "boundaryPatch.hpp"
#include "util.hpp"
#include "typeHelpers.hpp"
#include <iostream>
#include <ostream>

namespace hms {

#define REQUIRE_MEMBER_FUNC(FUNC_NAME) \
	static_assert(std::is_member_function_pointer_v< \
		decltype(&MeshType::FUNC_NAME) \
	>);
#define REQUIRE_RETURNS_2X1_COLUMN_VECTOR(FUNC_NAME) \
	static_assert( decltype(std::declval<MeshType>().FUNC_NAME(Index{})) \
		::ColsAtCompileTime == 1 ); \
	static_assert( decltype(std::declval<MeshType>().FUNC_NAME(Index{})) \
		::RowsAtCompileTime == 2 );


#define CHEAD(RETURN_TYPE) \
template<typename MeshType> RETURN_TYPE MeshBase<MeshType>::

CHEAD()
MeshBase() :
	m_edges { this->derived() },
	m_cells { this->derived() }
{
	REQUIRE_MEMBER_FUNC(vertexImp)
	REQUIRE_MEMBER_FUNC(edgeImp)
	REQUIRE_MEMBER_FUNC(cellImp)
	REQUIRE_MEMBER_FUNC(nVerticesImp)
	REQUIRE_MEMBER_FUNC(nEdgesImp)
	REQUIRE_MEMBER_FUNC(nInnerEdgesImp)
	REQUIRE_MEMBER_FUNC(nBoundaryEdgesImp)
	REQUIRE_MEMBER_FUNC(nCellsImp)
	REQUIRE_MEMBER_FUNC(nDomainCellsImp)
	REQUIRE_MEMBER_FUNC(nGhostCellsImp)
	REQUIRE_MEMBER_FUNC(domainAreaImp)
	REQUIRE_RETURNS_2X1_COLUMN_VECTOR(vertexImp)

	m_logger = log::getLogger();
	#define LOGGER_NOT_CREATED true
	assert( m_logger && LOGGER_NOT_CREATED );
	#undef LOGGER_NOT_CREATED
}

CHEAD()
MeshBase( MeshBase<MeshType>&& other ) noexcept {
	*this = std::move(other);
}

CHEAD(MeshBase<MeshType>&)
operator=( MeshBase<MeshType>&& other ) noexcept {
	if (this == &other)
		return *this;
	
	m_edges = std::move( other.m_edges );
	m_cells = std::move( other.m_cells );
	m_cells.mesh( this->derived() );
	m_edges.mesh( this->derived() );

	m_boundaryPatches = std::move(other.m_boundaryPatches);
	m_logger = std::move( other.m_logger );

	return *this;
}

#define FORWARD_FUNC_CALL_NOARG(NAME,RET) \
	CHEAD(auto) NAME() \
	const -> RET { \
		return this->derived().NAME##Imp(); \
	}

#define FORWARD_FUNC_CALL_INDEX(NAME,RET) \
	CHEAD(auto) NAME(Index edge) \
	const -> RET { \
		return this->derived().NAME##Imp(edge); \
	}

FORWARD_FUNC_CALL_NOARG(nVertices     ,Index)
FORWARD_FUNC_CALL_NOARG(nEdges        ,Index)
FORWARD_FUNC_CALL_NOARG(nInnerEdges   ,Index)
FORWARD_FUNC_CALL_NOARG(nBoundaryEdges,Index)
FORWARD_FUNC_CALL_NOARG(nCells        ,Index)
FORWARD_FUNC_CALL_NOARG(nDomainCells  ,Index)
FORWARD_FUNC_CALL_NOARG(nGhostCells   ,Index)

FORWARD_FUNC_CALL_NOARG(domainArea,scalar)

FORWARD_FUNC_CALL_INDEX(vertex, VertexType)
FORWARD_FUNC_CALL_INDEX(edge  , EdgeType)
FORWARD_FUNC_CALL_INDEX(cell  , CellType)

CHEAD(auto) edges() const -> const EdgesType& {
	return m_edges;
}

CHEAD(auto) cells() const -> const CellsType& {
	return m_cells;
}

CHEAD(auto) boundaryPatches() const -> const std::vector<BoundaryPatch>&
{
	return m_boundaryPatches;
}

CHEAD(auto) boundaryPatches() -> std::vector<BoundaryPatch>& {
	return m_boundaryPatches;
}


CHEAD(auto) boundaryPatch(const std::string& name) const -> const BoundaryPatch&
{
	auto it { findByName(m_boundaryPatches, name) };
	if ( it != m_boundaryPatches.cend() && it->name() == name )
		return *it;
	else
		throw std::runtime_error("Boundary patch \"" + name + "\" not found!");
}

CHEAD(auto) boundaryPatch(const std::string& name) -> BoundaryPatch&
{
	/* to avoid code duplication */
	return const_cast<BoundaryPatch&>(
		static_cast<const MeshBase<MeshType>&>(*this).boundaryPatch(name)
	);
}



CHEAD(auto) boundaryPatch_p(const std::string& name) const
	-> const BoundaryPatch&
{
	return *( findByName(m_boundaryPatches, name) );
}

CHEAD(void) addBoundaryPatch( std::string name, ArrayXi&& edges ){
	addBoundaryPatch(std::move(name), std::move(edges), ArrayXi{}, ArrayXXi{});
}

CHEAD(void) addBoundaryPatch( 
	std::string name,
	ArrayXi&& edges,
	ArrayXi&& domainCells,
	ArrayXXi&& ghostCells
){
	#ifndef NDEBUG
	for ( auto e : edges )
		if ( !edge(e).isBoundary() )
			throw hms::MeshError(
				"Failed to add patch \"" + name + "\": "
				"Edge " + std::to_string(e) + " is not a boundary edge!\n"
			);
	#endif
	
	/* empty boundary patches are allowed, because they may be required for 
	 * initialisation of mesh partitions */
	if ( edges.size() != 0 )
		this->derived().checkBoundaryPatch(edges);

	/* check whether boundary patch already exists, and if so, replace it */
	auto patch = findByName(m_boundaryPatches, name);
	if ( patch != m_boundaryPatches.cend() && patch->name() == name ){
		logger().warn(
			"Patch with name \"{}\" already exists! "
			"Adding a patch with an existing name "
			"overwrites the previous patch with that name! "
			"If that is not your intention, choose unique names instead.\n"
			"Existing patch has edges\n\t{}\n"
			"New patch has edges\n\t{}",
			name, patch->edges().transpose(), edges.transpose()
		);
		m_boundaryPatches.erase(patch);
	}

	/* depending on whether or not domain and ghost cells have been passed in,
	 * use the appropriate constructor.
	 * To be able to find boundary patches with binary search, the vector must
	 * be sorted, so insertion is used. */
	if ( domainCells.size() != 0 && ghostCells.size() != 0 )
		m_boundaryPatches.insert(
			patch,
			{	std::move(name),
				this->derived(),
				std::move(edges),
				std::move(domainCells),
				std::move(ghostCells) }
		);
	else
		m_boundaryPatches.insert(
			patch,
			{ std::move(name), this->derived(), std::move(edges) }
		);
}

CHEAD(void) addBoundaryPatches(
	std::vector<BoundaryPatch>&& patches
){
	m_boundaryPatches = std::move(patches);
}

CHEAD(void) clearBoundaryPatches(){
	m_boundaryPatches.clear();
}

CHEAD(void) eraseBoundaryPatch( const std::string& name ){
	auto it { findByName(m_boundaryPatches, name) };
	if ( it != m_boundaryPatches.cend() && it->name() == name )
		m_boundaryPatches.erase( it );
	else
		logger().warn(
			"Requested to erase patch \"{}\", but no such patch was found!",
			name
		);
}


CHEAD(void) printInfo() const {
	fmt::memory_buffer out;
	fmt::format_to( std::back_inserter(out),
		"Mesh:"
		"\n{}Type: \"{}\"", indent(), type_name<MeshType>()
	);
	auto printSubHeader = [&](std::string_view desc){
		fmt::format_to( std::back_inserter(out), "\n{}{}:", indent(), desc );
	};
	printSubHeader("Cells");

	scalar area { domainArea() };
	int
		numColWidth {
			/* the 9 is for the format #.###e-## for the area */
			std::max(9, maxDecimalDigits( nVertices(), nEdges(), nCells() ) )
		},
		descColWidth {13};
	auto printSubProperty = [&](
		std::string_view desc,
		const auto& val,
		const std::string& unitOrComment = {}
	){
		fmt::format_to(std::back_inserter(out),
			"\n{0}{2:<{1}} {4:>{3}} {5}",
			indent(2),
			descColWidth, desc,
			numColWidth , val,
			unitOrComment
		);
	};
	printSubProperty( "total:", nCells() );
	printSubProperty( "domain cells:", nDomainCells() );

	/* insert nCellsX x nCellsY for structured meshes */
	if constexpr ( std::is_base_of_v<StructMeshBase<MeshType>, MeshType> ){
		fmt::format_to(std::back_inserter(out),
			"({}x{})",
			this->derived().nCellsX(),
			this->derived().nCellsY()
		);
	}
	printSubProperty( "ghost cells:", nGhostCells() );

	/* insert cell size for UniMesh types */
	if constexpr ( std::is_same_v<MeshType, UniMesh> ){
		printSubProperty(
			"cell size:",
			this->derived().dx() * this->derived().dy(),
			fmt::format(
				"({}x{}) sqm",
				this->derived().dx(),
				this->derived().dy()
			)
		);
	}
	fmt::format_to(std::back_inserter(out),
		"\n{0}{2:<{1}} {4:>{3}.3e} {5}",
		indent(2),
		descColWidth, "domain area:",
		numColWidth , area, "sqm"
	);
	
	printSubHeader("Edges");
	printSubProperty( "total:", nEdges() );
	printSubProperty( "inner:", nInnerEdges() );
	printSubProperty( "boundary:", nBoundaryEdges() );

	printSubHeader("Vertices");
	printSubProperty( "total:", nVertices() );

	printSubHeader("Boundary");
	if ( m_boundaryPatches.empty() ){
		fmt::format_to(std::back_inserter(out),
			" No patches defined.", indent() );
	} else {
		fmt::format_to(std::back_inserter(out),
			" Contains {} patches.",
			m_boundaryPatches.size()
		);
		for ( const BoundaryPatch& patch : m_boundaryPatches ){
			printSubProperty(patch.name(), patch.size(), "edges");
		}
	}
	logger().info( fmt::to_string(out) );
}

CHEAD(bool) empty() const {
	return this->nCells() == 0;
}

CHEAD(log::logger&) logger() const {
	assert( m_logger );
	return *m_logger;
}


#undef REQUIRE_MEMBER_FUNC
#undef REQUIRE_RETURNS_2X1_COLUMN_VECTOR
#undef CHEAD
#undef FORWARD_FUNC_CALL_NOARG
#undef FORWARD_FUNC_CALL_INDEX

template class MeshBase<Mesh>;
template class MeshBase<UniMesh>;
template class MeshBase<RectMesh>;
template class MeshBase<StructMesh>;

} // end namespace hms
